/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.context.embedded;

import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import javax.servlet.Filter;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.ServletContextInitializerBeans;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.context.support.ServletContextAwareProcessor;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.context.support.ServletContextScope;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 * A {@link WebApplicationContext} that can be used to bootstrap itself from a contained
 * {@link EmbeddedServletContainerFactory} bean.
 * <p>
 * This context will create, initialize and run an {@link EmbeddedServletContainer} by
 * searching for a single {@link EmbeddedServletContainerFactory} bean within the
 * {@link ApplicationContext} itself. The {@link EmbeddedServletContainerFactory} is free
 * to use standard Spring concepts (such as dependency injection, lifecycle callbacks and
 * property placeholder variables).
 * <p>
 * In addition, any {@link Servlet} or {@link Filter} beans defined in the context will be
 * automatically registered with the embedded Servlet container. In the case of a single
 * Servlet bean, the '/' mapping will be used. If multiple Servlet beans are found then
 * the lowercase bean name will be used as a mapping prefix. Any Servlet named
 * 'dispatcherServlet' will always be mapped to '/'. Filter beans will be mapped to all
 * URLs ('/*').
 * <p>
 * For more advanced configuration, the context can instead define beans that implement
 * the {@link ServletContextInitializer} interface (most often
 * {@link ServletRegistrationBean}s and/or {@link FilterRegistrationBean}s). To prevent
 * double registration, the use of {@link ServletContextInitializer} beans will disable
 * automatic Servlet and Filter bean registration.
 * <p>
 * Although this context can be used directly, most developers should consider using the
 * {@link AnnotationConfigEmbeddedWebApplicationContext} or
 * {@link XmlEmbeddedWebApplicationContext} variants.
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @since 1.0.0
 * @see AnnotationConfigEmbeddedWebApplicationContext
 * @see XmlEmbeddedWebApplicationContext
 * @see EmbeddedServletContainerFactory
 */
public class EmbeddedWebApplicationContext extends GenericWebApplicationContext {

	private static final Log logger = LogFactory.getLog(EmbeddedWebApplicationContext.class);

	/**
	 * Constant value for the DispatcherServlet bean name. A Servlet bean with this name
	 * is deemed to be the "main" servlet and is automatically given a mapping of "/" by
	 * default. To change the default behaviour you can use a
	 * {@link ServletRegistrationBean} or a different bean name.
	 */
	public static final String DISPATCHER_SERVLET_NAME = "dispatcherServlet";

	private volatile EmbeddedServletContainer embeddedServletContainer;

	private ServletConfig servletConfig;

	private String namespace;

	/**
	 * Register ServletContextAwareProcessor.
	 * @see ServletContextAwareProcessor
	 */
	@Override
	protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
		// WebApplicationContextServletContextAwareProcessor被用于向bean注入ConfigurableWebApplicationContext应用上下文，它仅仅用在当前应用是一个Springboot + Spring MVC(也就是Servlet)Web应用的情况下。
		// 如果当前应用是一个非Spring boot Servlet Web应用，而是一个非Servlet应用，或者非Web的命令行应用，则WebApplicationContextServletContextAwareProcessor根本不会出现在应用上下文中。
		beanFactory.addBeanPostProcessor(new WebApplicationContextServletContextAwareProcessor(this));
		beanFactory.ignoreDependencyInterface(ServletContextAware.class);
		registerWebApplicationScopes();
	}

	@Override
	public final void refresh() throws BeansException, IllegalStateException {
		try {
			// 调用父类，也就是AbstractApplicationContext的refresh方法
			// 父类的refresh方法是个模板方法，其中执行了很多抽象方法，子类根据情况重写抽象方法，达到模板化refresh整个应用上下文
			super.refresh();
		}
		catch (RuntimeException ex) {
			// 如果有异常，则停止内嵌的ServleyWebServer
			stopAndReleaseEmbeddedServletContainer();
			throw ex;
		}
	}

	@Override
	protected void onRefresh() {
		// 调用父类的onRefresh方法(GenericWebApplicationContext)
		super.onRefresh();
		try {
			//核心方法：会获取嵌入式的Servlet容器工厂，并通过工厂来获取Servlet容器
			createEmbeddedServletContainer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start embedded container", ex);
		}
	}

	@Override
	protected void finishRefresh() {
		// 调用父类完成刷新逻辑
		super.finishRefresh();
		// 调用启动EmbeddedServletContainer的方法
		EmbeddedServletContainer localContainer = startEmbeddedServletContainer();
		// 如果启动成功，会返回localContainer
		if (localContainer != null) {
			// 启动成功，发布EmbeddedServletContainerInitializedEvent事件
			publishEvent(new EmbeddedServletContainerInitializedEvent(this, localContainer));
		}
	}

	@Override
	protected void onClose() {
		super.onClose();
		stopAndReleaseEmbeddedServletContainer();
	}

	/**执行这个方法时当前应用上下文的所有BeanDefinition都已经加载，且对bean进行初始化的相关组件如BeanFactoryPostProcessor和BeanPostProcessor也都已经初始化完成。也即整个
	 * <br/>bean创建的生命周期中所需要的组件都已准备就绪，此时无论需要使用任何bean，都可以完整的创建出来
	 * <br/>所以在这个时机加载内嵌servlet容器非常合理，容器中需要使用到的bean都可以被完整的初始化，可以保证SpringApplication生命周期完整和servlet容器生命周期的完整。
	 */
	private void createEmbeddedServletContainer() {
		// 先获取embeddedServletContainer
		EmbeddedServletContainer localContainer = this.embeddedServletContainer;
		// 获取Servlet上下文
		ServletContext localServletContext = getServletContext();
		// EmbeddedWebApplicationContext没有为EmbeddedServletContainer和ServletContext赋初值，之前也没有调用set方法，所以这里都是为null
		if (localContainer == null && localServletContext == null) {
			// 先获取嵌入式Servlet容器工厂
			EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
			// 根据容器工厂来获取对应的嵌入式Servlet容器
			this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(getSelfInitializer());
		}
		else if (localServletContext != null) {
			try {
				getSelfInitializer().onStartup(localServletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context", ex);
			}
		}
		// 初始化属性信息，用于使用ServletContext和ServletConfig代替ApplicationContext的Environment中的这两个属性源
		initPropertySources();
	}

	/**
	 * Returns the {@link EmbeddedServletContainerFactory} that should be used to create
	 * the embedded servlet container. By default this method searches for a suitable bean in the context itself.
	 * @return a {@link EmbeddedServletContainerFactory} (never {@code null})
	 */
	protected EmbeddedServletContainerFactory getEmbeddedServletContainerFactory() {
		// 从BeanFactory中获取EmbeddedServletContainerFactory类型的Bean，这里没有考虑父BeanFactory
		// 那么EmbeddedServletContainerFactory的Bean是什么时候被注入到Spring容器中的呢？
		// 在SpringBoot中为我们内置了三种Web服务器的实现类，TomCat、Jetty、Undertow(没接触过)。我们之前说SpringBoot的四大神器，其中之一是自动配置的功能，在SpringBoot中有这样一个类EmbeddedServletContainerAutoConfiguration
		// 这个类配置在spring.factories中，它的key为org.springframework.boot.autoconfigure.EnableAutoConfiguration，这里你需要知道，在SpringBoot启动的时候会加载这个类，并且这个类上带有Configuration这个注解，所以这个类会被注入到Spring容器中，然后这个类要生效还要有一个条件，即当前环境是web环境！
		// 如果当前类路径下有Servlet和Tomcat这两个类，且在当前上下文中没有EmbeddedServletContainerFactory类型的Bean存在，则创建TomcatEmbeddedServletContainerFactory对象并注入到Spring容器中。SpringBoot中内置了TomCat相关的jar(spring-boot-starter-tomcat)。所以这里注入到Spring容器中的EmbeddedServletContainerFactory类型的Bean是TomcatEmbeddedServletContainerFactory
		// 同时还@Import(EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar.class)，注册了embeddedServletContainerCustomizerBeanPostProcessor和errorPageRegistrarBeanPostProcessor
		String[] beanNames = getBeanFactory().getBeanNamesForType(EmbeddedServletContainerFactory.class);
		// 如果没有获取到EmbeddedServletContainerFactory类型的Bean，则抛出异常
		if (beanNames.length == 0) {
			throw new ApplicationContextException("Unable to start EmbeddedWebApplicationContext due to missing "
					+ "EmbeddedServletContainerFactory bean.");
		}
		// 如果有一个以上的EmbeddedServletContainerFactory类型的Bean，则抛出异常
		if (beanNames.length > 1) {
			throw new ApplicationContextException("Unable to start EmbeddedWebApplicationContext due to multiple "
					+ "EmbeddedServletContainerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
		}
		// 从BeanFactory中获取EmbeddedServletContainerFactory类型的Bean
		return getBeanFactory().getBean(beanNames[0], EmbeddedServletContainerFactory.class);
	}

	/**
	 * Returns the {@link ServletContextInitializer} that will be used to complete the
	 * setup of this {@link WebApplicationContext}.
	 * @return the self initializer
	 * @see #prepareEmbeddedWebApplicationContext(ServletContext)
	 */
	private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
		// 创建一个ServletContextInitializer对象，并重写onStartup方法，很明显是一个回调方法
		// 因为有些操作需要在Servlet容器初始化完成的时候执行，Initializer的作用就是如此，在它的onStartup逻辑中会调用selfInitialize方法
		// 这个方法会注册一些Servlet上下文特有的组件和作用域，同时获取ApplicationContext中的所有ServletContextInitializer类型的bean，统一调用它们的onStartup方法
		return new ServletContextInitializer() {
			@Override
			public void onStartup(ServletContext servletContext) throws ServletException {
				// springboot之ServletContextInitializerBeans如何将Filter、Servlet、EventListener注册到ServletContext中？就在selfInitialize(servletContext);
				selfInitialize(servletContext);
			}
		};
	}

	/**
	 * @param servletContext
	 * @throws ServletException 已经完全加载的ServletContext
	 */
	private void selfInitialize(ServletContext servletContext) throws ServletException {
		// 准备WebApplicationContext，互换ServletContext和ApplicationContext中的一些属性(把EmbeddedWebApplicationContext存入ServletContext的attr，把ServletContext放入EmbeddedWebApplicationContext的servletContext属性)
		prepareEmbeddedWebApplicationContext(servletContext);
		// 通过ServletContextScope包装servletContext，并将其注册为全局web应用范围（"application"）对应的值和注册为servletContext类的属性
		registerApplicationScope(servletContext);
		// 把ServletContext和ServletConfig注册为单例
		WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
		// getServletContextInitializerBeans: 获取所有的ServletContextInitializerBeans实例对象，并调用onStartup
		for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
			// 现在所有的ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean都添加到List<ServletContextInitializer> sortedList这个集合中去了，接着就是遍历这个集合，执行其onStartup方法了
			beans.onStartup(servletContext);
		}
	}

	private void registerApplicationScope(ServletContext servletContext) {
		ServletContextScope appScope = new ServletContextScope(servletContext);
		getBeanFactory().registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
		// Register as ServletContext attribute, for ContextCleanupListener to detect it.
		servletContext.setAttribute(ServletContextScope.class.getName(), appScope);
	}

	private void registerWebApplicationScopes() {
		ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes(getBeanFactory());
		WebApplicationContextUtils.registerWebApplicationScopes(getBeanFactory());
		existingScopes.restore();
	}

	/**
	 * Returns {@link ServletContextInitializer}s that should be used with the embedded
	 * Servlet context. By default this method will first attempt to find
	 * {@link ServletContextInitializer}, {@link Servlet}, {@link Filter} and certain
	 * {@link EventListener} beans.
	 * @return the servlet initializer beans
	 */
	protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
		// ServletContextInitializerBeans其实就是一个从ListableBeanFactory中获得的ServletContextInitializer的集合，包括所有ServletContextInitializer的bean，还适用于Servlet、Filter和某些EventListener的bean
		return new ServletContextInitializerBeans(getBeanFactory());
	}

	/**
	 * Prepare the {@link WebApplicationContext} with the given fully loaded
	 * {@link ServletContext}. This method is usually called from
	 * {@link ServletContextInitializer#onStartup(ServletContext)} and is similar to the
	 * functionality usually provided by a {@link ContextLoaderListener}.
	 * @param servletContext the operational servlet context
	 */
	protected void prepareEmbeddedWebApplicationContext(ServletContext servletContext) {
		Object rootContext = servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
		if (rootContext != null) {
			// ServletContextInitializers不能被多次注册
			if (rootContext == this) {
				throw new IllegalStateException(
						"Cannot initialize context because there is already a root application context present - "
								+ "check whether you have multiple ServletContextInitializers!");
			}
			return;
		}
		Log logger = LogFactory.getLog(ContextLoader.class);
		servletContext.log("Initializing Spring embedded WebApplicationContext");
		try {
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);
			if (logger.isDebugEnabled()) {
				logger.debug("Published root WebApplicationContext as ServletContext attribute with name ["
						+ WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
			}
			setServletContext(servletContext);
			if (logger.isInfoEnabled()) {
				long elapsedTime = System.currentTimeMillis() - getStartupDate();
				logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
			}
		}
		catch (RuntimeException ex) {
			logger.error("Context initialization failed", ex);
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
			throw ex;
		}
		catch (Error ex) {
			logger.error("Context initialization failed", ex);
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
			throw ex;
		}
	}

	private EmbeddedServletContainer startEmbeddedServletContainer() {
		EmbeddedServletContainer localContainer = this.embeddedServletContainer;
		if (localContainer != null) {
			// 调用启动方法，会对之前创建的Tomcat进行启动，调用TomcatEmbeddContext的loadOnStartup方法，启动整个StandardContext
			localContainer.start();
		}
		return localContainer;
	}

	private void stopAndReleaseEmbeddedServletContainer() {
		EmbeddedServletContainer localContainer = this.embeddedServletContainer;
		if (localContainer != null) {
			try {
				localContainer.stop();
				this.embeddedServletContainer = null;
			}
			catch (Exception ex) {
				throw new IllegalStateException(ex);
			}
		}
	}

	@Override
	protected Resource getResourceByPath(String path) {
		if (getServletContext() == null) {
			return new ClassPathContextResource(path, getClassLoader());
		}
		return new ServletContextResource(getServletContext(), path);
	}

	@Override
	public void setNamespace(String namespace) {
		this.namespace = namespace;
	}

	@Override
	public String getNamespace() {
		return this.namespace;
	}

	@Override
	public void setServletConfig(ServletConfig servletConfig) {
		this.servletConfig = servletConfig;
	}

	@Override
	public ServletConfig getServletConfig() {
		return this.servletConfig;
	}

	/**
	 * Returns the {@link EmbeddedServletContainer} that was created by the context or
	 * {@code null} if the container has not yet been created.
	 * @return the embedded servlet container
	 */
	public EmbeddedServletContainer getEmbeddedServletContainer() {
		return this.embeddedServletContainer;
	}

	/**
	 * Utility class to store and restore any user defined scopes. This allow scopes to be
	 * registered in an ApplicationContextInitializer in the same way as they would in a
	 * classic non-embedded web application context.
	 */
	public static class ExistingWebApplicationScopes {

		private static final Set<String> SCOPES;

		static {
			Set<String> scopes = new LinkedHashSet<String>();
			scopes.add(WebApplicationContext.SCOPE_REQUEST);
			scopes.add(WebApplicationContext.SCOPE_SESSION);
			scopes.add(WebApplicationContext.SCOPE_GLOBAL_SESSION);
			SCOPES = Collections.unmodifiableSet(scopes);
		}

		private final ConfigurableListableBeanFactory beanFactory;

		private final Map<String, Scope> scopes = new HashMap<String, Scope>();

		public ExistingWebApplicationScopes(ConfigurableListableBeanFactory beanFactory) {
			this.beanFactory = beanFactory;
			for (String scopeName : SCOPES) {
				Scope scope = beanFactory.getRegisteredScope(scopeName);
				if (scope != null) {
					this.scopes.put(scopeName, scope);
				}
			}
		}

		public void restore() {
			for (Map.Entry<String, Scope> entry : this.scopes.entrySet()) {
				if (logger.isInfoEnabled()) {
					logger.info("Restoring user defined scope " + entry.getKey());
				}
				this.beanFactory.registerScope(entry.getKey(), entry.getValue());
			}
		}

	}

}
